fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing [P1]#1960
fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing [P1]#1960miguel-heygen wants to merge 2 commits into
Conversation
…re writing
synthesizeOne's elevenlabs branch spawns a Python helper that writes
straight to wavAbs, but — unlike the heygen path (which mkdirs via
transcodeToWav / directly) and the kokoro path (where the `hyperframes
tts` CLI owns the output dir) — it never created the parent directory.
On a fresh project with no assets/voice/ yet, the Python save failed and
the line was silently dropped as "TTS failed - omitted", so every
ElevenLabs line vanished until the user hand-created assets/voice.
Fix: mkdirSync(dirname(wavAbs), { recursive: true }) before spawning,
matching the other providers.
Test: new tts.test.mjs case drives synthesizeOne(elevenlabs) with a
nested, not-yet-created wavAbs and asserts the output dir exists
afterward — independent of whether the (keyless) Python actually writes
a file, since the mkdir runs before the spawn either way.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 7d7470e.
Clean, minimal fix that lands exactly where the asymmetry was, matches the shape of the two sibling providers, and comes with a test that survives even without an ELEVENLABS_API_KEY. LGTM from my side — leaving as a comment. Just two very small notes.
Concerns
- Uncaught
mkdirSyncthrow on read-only parent.tts.mjs:224throws on EACCES / EROFS instead of returning the same{ ok: false }shape the rest of the elevenlabs branch does — andaudio.mjs:148doesn't wrap thesynthesizeOnecall in a try/catch, so a hard filesystem error would take down the wholemapWithConcurrencybatch instead of degrading one line to "TTS failed — omitted." The old code's silent-Python-fail path would have absorbed this. Not a blocker — read-onlyassets/voice/is a very unusual failure mode and the fresh-project case (which is what the reporter hit) is the load-bearing one this PR fixes — but wrapping themkdirSyncin a tiny try/catch that returns{ ok: false, words: null }preserves the pre-PR error surface for the pathological cases too.heygen'stranscodeToWavattts.mjs:181has the same issue for what it's worth — worth aligning both to atry/catch → ok:falseshape if you feel like a follow-up.
Nits
- The regression test only asserts
existsSync(dirname(wavAbs))— great for confirming the directory materializes, but doesn't guard against a future refactor that hoists themkdirSyncto a code path that skips the elevenlabs branch (e.g. moved up tosynthesizeOne's prelude before theif (provider === ...)fork). A follow-on assert that the mkdir survives whenprovider !== "elevenlabs"isn't called would tighten the contract, though it's arguably over-engineering for a one-liner. (nit)
What I didn't verify
- The three flagged follow-ups in the PR body (Windows npx
npm_execpath, Lambda__dirname-in-ESM boot crash, static-dedup freeze). Read them as parked, not part of this PR's scope.
— Review by Rames D Jusso
synthesizeOne's contract is 'never throws; failures return { ok:false }', but
the new mkdirSync could throw on EACCES/EROFS. Wrap it so a mkdir failure
returns { ok:false } like the rest of the branch, restoring parity.
|
Superseded by #… (the migrated fix). This branch targets |
|
Migrated fix is #2032. |
The ElevenLabs provider spawns a Python helper that writes straight to wavAbs
via a bare open(), which (unlike heygen/kokoro) never creates the parent dir —
so on a fresh project the save throws ENOENT and the line is silently dropped
as 'TTS failed - omitted'. mkdir -p the dir first, guarded so a mkdir failure
(EACCES/EROFS) returns { ok:false } like the rest of the branch rather than
throwing. (Migrated from #1960, whose skills/hyperframes-media path was retired
into skills/media-use; the bug moved with it.)
The ElevenLabs provider spawns a Python helper that writes straight to wavAbs
via a bare open(), which (unlike heygen/kokoro) never creates the parent dir —
so on a fresh project the save throws ENOENT and the line is silently dropped
as 'TTS failed - omitted'. mkdir -p the dir first, guarded so a mkdir failure
(EACCES/EROFS) returns { ok:false } like the rest of the branch rather than
throwing. (Migrated from #1960, whose skills/hyperframes-media path was retired
into skills/media-use; the bug moved with it.)
Root cause
synthesizeOne's elevenlabs branch spawns a Python helper that writes straight towavAbs, but — unlike the heygen path (whichmkdirs viatranscodeToWav/ directly) and the kokoro path (where thehyperframes ttsCLI owns the output dir) — it never created the parent directory. On a fresh project with noassets/voice/yet, the Pythonsavefailed and the line was silently dropped as "TTS failed - omitted", so every ElevenLabs line vanished until the user hand-createdassets/voice.Reported precisely (the reporter traced it to the missing
mkdirSync(dirname(wavAbs))that the other two providers have).Fix
mkdirSync(dirname(wavAbs), { recursive: true })before spawning, matching the other providers. One line;dirname/mkdirSyncwere already imported.Test plan
New
tts.test.mjscase drivessynthesizeOne({ provider: "elevenlabs", … })with a nested, not-yet-createdwavAbsand asserts the output dir exists afterward — independent of whether the (keyless) Python actually writes a file, since themkdirruns before the spawn either way.node --test skills/hyperframes-media/scripts/lib/tts.test.mjs— 6/6 passing.node --check+oxlint/oxfmtclean; fullbun run buildclean.From the same ~20h feedback backlog: PR #1959 (Windows symlink EPERM) got a 2nd independent confirmation, and several "spawn EINVAL" reports corroborate PR #1937 (ffmpeg .cmd-vs-.exe). Other precise items logged for follow-up: a Windows npx/
npm_execpathsilent-TTS-failure, a Lambda__dirname-in-ESM boot crash (build-zip.tsbanner), and a static-dedup freeze for<template>-mounted sub-comps on a zero-tween host.